﻿// /////////////////////////////////////////////////////////////////////
// 
//  Codeplex JSAnalyse
//  Copyright (c) 2012
//  JSAnalyse.Validator   Validator.cs
// 
// /////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using JSAnalyse.Core;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Layer;
using Microsoft.VisualStudio.GraphModel;
using Graph = Microsoft.VisualStudio.GraphModel.Graph;
using System.Xml;

namespace JSAnalyse.Validator
{
    /// <summary>
    /// Validation Extension for the Visual Studio Layer Diagram
    /// </summary>
    [Export(typeof(IValidateArchitectureExtension))]
    public class ValidatorExtension : IValidateArchitectureExtension
    {
        #region Private variables
        private readonly List<AllowedDependency> _allowedDependencyList = new List<AllowedDependency>();
        private readonly List<string> _filesToCheck = new List<string>();
        private Dictionary<string, XmlDocument> _xmlDocuments = new Dictionary<string, XmlDocument>();
        #endregion

        #region Interface Member
        /// <summary>
        /// Validates if the defined architecture for the Javascript files is followed.
        /// </summary>
        /// <param name="graph">The graph</param>
        public void ValidateArchitecture(Graph graph)
        {
            // System.Diagnostics.Debugger.Launch();

            if (graph == null)
            {
                throw new ArgumentNullException("graph");
            }

            GraphNode firstLayer = null;
            var layersWithJs = new List<GraphNode>();

            // Get all layers on the diagram
            // Loop through all reference files and collect them
            foreach (var layer in graph.Nodes.GetByCategory("Dsl.Layer"))
            {
                if (firstLayer == null)
                {
                    firstLayer = layer;
                }

                var files = layer.FindDescendants().Where(item => item.HasCategory("Dsl.Reference") && IsJsFile(item)).ToList();

                if (files.Count > 0)
                {
                    layersWithJs.Add(layer);

                    foreach (var file in files)
                    {
                        var filePath = GetPath(file, graph);
                        AddToFilePathList(filePath);
                    }
                }
            }

            // Find all links and build dependency graph
            foreach (var layer in layersWithJs)
            {
                var links = layer.AllLinks.Where(item => item.HasCategory("Dsl.DependencyFromLayerToLayer")).ToList();

                foreach (var link in links)
                {
                    FindDependencies(graph, link);
                }
            }

            if (_filesToCheck.Count > 0)
            {
                // Start analysis
                var result = new DependencyRunner(_filesToCheck, new AllowedDependenciesLayerDiagram(_allowedDependencyList)).Start();
                WriteErrorOutput(result, graph, firstLayer);
            }
        }
        #endregion

        #region Private methods
        private void FindDependencies(Graph completeGraph, GraphLink link)
        {
            var source = link.Source;
            var target = link.Target;

            foreach (var sourceFile in source.FindDescendants())
            {
                if (IsJsFile(sourceFile))
                {
                    var sourceFilePath = GetPath(sourceFile, completeGraph);

                    foreach (var targetFile in target.FindDescendants())
                    {
                        if (IsJsFile(targetFile))
                        {
                            string targetFilePath = GetPath(targetFile, completeGraph);
                            AddToDependencyList(sourceFilePath, targetFilePath);
                        }
                    }
                }
            }
        }

        private void WriteErrorOutput(IEnumerable<DependencyRunnerResult> result, Graph graph, GraphNode firstLayer)
        {
            // Get all referenced types in this layer including those from nested layers so each
            // type is validated against all containing layer constraints.
            foreach (var item in result)
            {
                if (item.WrongFileDependencies.Count > 0)
                {
                    foreach (var wrongDependency in item.WrongFileDependencies)
                    {
                        string message = string.Format("File {0} has wrong Dependency to File {1} in Line {2}, Character {3}\n\nSource: {4}",
                                      item.SourceFile,
                                      wrongDependency.File,
                                      wrongDependency.DependencySourceLocation.Line,
                                      wrongDependency.DependencySourceLocation.Character,
                                      wrongDependency.DependencySourceLocation.SourceLine);
                        var allTypes = graph.Nodes.GetByCategory("Dsl.Layer");

                        this.LogValidationError(graph, "JsDependencyError", message, GraphErrorLevel.Error, allTypes.First());
                    }
                }
            }
        }

        private bool IsJsFile(GraphNode node)
        {
            if (node != null && node.Label != null && node.Label.EndsWith(".js"))
            {
                return true;
            }
            return false;
        }

        private void AddToDependencyList(string sourceFile, string targetFile)
        {
            if (_allowedDependencyList.FirstOrDefault(item => item.SourceFile == sourceFile && item.TargetFile == targetFile) == null)
            {
                _allowedDependencyList.Add(new AllowedDependency { SourceFile = sourceFile, TargetFile = targetFile });
            }
        }

        private void AddToFilePathList(string targetFile)
        {
            if (File.Exists(targetFile) && !_filesToCheck.Contains(targetFile))
            {
                _filesToCheck.Add(targetFile);
            }
        }

        private string GetPath(GraphNode sourceFile, Graph graph)
        {
            var result = string.Empty;

            var artifactPath = string.Empty;
            var layerDiagramPath = string.Empty;

            var layerDiagramList = graph.Nodes.GetByCategory("File").Where(item => item.DescriptiveCategoryLabel == "File").ToList();
            if (layerDiagramList.Count > 0)
            {
                foreach (var layerDiagram in layerDiagramList)
                {
                    layerDiagramPath = ConvertToValidUncPath(layerDiagram.Id.LiteralValue);

                    var xmlDocument = LoadLayerDiagram(layerDiagramPath);
                    if (xmlDocument != null)
                    {
                        var names = new XmlNamespaceManager(xmlDocument.NameTable);
                        names.AddNamespace("ld", "http://schemas.microsoft.com/VisualStudio/TeamArchitect/LayerDesigner");

                        var nodes =
                            xmlDocument.SelectNodes(string.Format("//ld:reference[@Id='{0}']/ld:ArtifactNode", sourceFile.Id.Value), names);
                        if (nodes != null && nodes.Count > 0)
                        {
                            var xmlNode = nodes[0];
                            if (xmlNode != null && xmlNode.Attributes != null && xmlNode.Attributes["Id"] != null)
                            {
                                artifactPath = xmlNode.Attributes["Id"].Value;
                                break;
                            }
                        }
                    }
                }
            }
            if (!String.IsNullOrEmpty(layerDiagramPath))
            {
                result = GetJsFilePath(artifactPath, layerDiagramPath);
            }
            return result;
        }

        private XmlDocument LoadLayerDiagram(string layerDiagramFilePath)
        {
            if (!_xmlDocuments.ContainsKey(layerDiagramFilePath))
            {
                var loadedXmlDocument = new XmlDocument();
                loadedXmlDocument.Load(layerDiagramFilePath);
                _xmlDocuments.Add(layerDiagramFilePath, loadedXmlDocument);
            }
            return _xmlDocuments[layerDiagramFilePath];
        }

        private string GetJsFilePath(string relativeJsPath, string diagramFilePath)
        {
            var diagramPath = Path.GetDirectoryName(diagramFilePath.Replace("file:///", ""));

            var result = ConvertToValidUncPath(relativeJsPath);
            result = Path.Combine(diagramPath ?? string.Empty, result).Replace("/", "\\");
            return Path.GetFullPath(result);
        }

        /// <summary>
        /// Converts the (Assembly=...) Path to a valid unc path
        /// e.g.
        /// (Assembly=file:///Scripts/Script.js)
        /// will be converted to
        /// file:///Scripts/Script.js
        /// </summary>
        /// <param name="relativeJsPath"></param>
        /// <returns></returns>
        private static string ConvertToValidUncPath(string relativeJsPath)
        {
            string result = relativeJsPath.Replace("(Assembly=", string.Empty);
            if (result.EndsWith(")"))
            {
                result = result.Substring(0, result.Length - 1);
            }
            return result;
        }

        #endregion
    }
}